package handler

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/go-redis/redis/v8"
	req2 "github.com/imroc/req/v3"
	"gorm.io/gorm"
	"html/template"
	"io"
	"myai/core"
	"myai/core/types"
	"myai/service"
	"myai/service/oss"
	"myai/store/model"
	"myai/store/model/rsp"
	"myai/store/vo"
	"myai/utils"
	"net/http"
	"net/url"
	"regexp"
	"strings"
	"time"
	"unicode/utf8"
)

type ChatHttpHandler struct {
	BaseHandler
	redis         *redis.Client
	uploadManager *oss.UploaderManager
	ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
	ChatContexts  *types.LMap[string, []interface{}]      // 聊天上下文 Map[chatId] => []Message
	userService   *service.UserService
}

func NewChatHttpHandler(app *core.AppServer, db *gorm.DB, redisCli *redis.Client, uploadManager *oss.UploaderManager,
	userService *service.UserService) *ChatHttpHandler {
	return &ChatHttpHandler{
		BaseHandler:   BaseHandler{App: app, DB: db},
		redis:         redisCli,
		uploadManager: uploadManager,
		userService:   userService,
		ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
		ChatContexts:  types.NewLMap[string, []interface{}](),
	}
}

// sendMessage 向大模型发送请求
func (h *ChatHttpHandler) sendMessage(ctx context.Context, session *types.ChatSession, role model.ChatRole, prompt string, ws *types.WsClient) error {
	// 不是 debug 模式下，捕获panic错误
	if !h.App.Debug {
		defer func() {
			if r := recover(); r != nil {
				log.Errorf("sendMessage 向大模型发送消息失败：%v", r)
			}
		}()
	}
	var user model.User
	// 查询用户信息
	res := h.DB.Model(&model.User{}).First(&user, session.UserId)
	if res.Error != nil {
		return errors.New("未授权用户，您正在进行非法操作！")
	}

	var userVo vo.User
	err := utils.CopyObject(user, &userVo)
	userVo.Id = user.Id
	if err != nil {
		return errors.New("User 对象转换失败，" + err.Error())
	}

	if userVo.Status == false {
		return errors.New("您的账号已经被禁用，如有疑问，请联系管理员！")
	}

	// 判断用户的算力是否小于模型使用一次所消耗的算力
	if userVo.Power < session.Model.Power {
		return fmt.Errorf("您当前剩余算力 %d 已不足以支付当前模型的单次对话所需要消耗的算力 %d，[立即购买](/member)。", userVo.Power, session.Model.Power)
	}

	// 计算提示词消耗的token
	promptTokens, err := utils.CalcTokens(prompt, session.Model.Value)
	if err != nil {
		return fmt.Errorf("计算提示词的token失败：%v", err)
	}

	// 检查prompt长度是否超过了当前模型允许的最多上下文长度
	if promptTokens > session.Model.MaxContext {
		return errors.New("对话内容超出了当前模型允许的最大上下文长度！")
	}

	// 构建大模型请求参数
	var req = types.ApiRequest{
		Model: session.Model.Value,
	}

	// 兼容 GPT-O1 模型
	if strings.HasPrefix(session.Model.Value, "o1-") {
		// 发送中间状态给前端
		utils.SendChunkMsg(ws, "AI 正在思考...\n")
		req.Stream = false
		session.Start = time.Now().Unix()
	} else {
		req.MaxTokens = session.Model.MaxTokens
		req.Temperature = session.Model.Temperature
		req.Stream = session.Stream
	}
	// 加载函数
	if len(session.Tools) > 0 && !strings.HasPrefix(session.Model.Value, "o1-") {
		var items []model.Function
		// 查询函数工具
		res = h.DB.Where("enabled", true).Where("id IN ?", session.Tools).Find(&items)
		if res.Error == nil {
			var tools = make([]types.Tool, 0)
			for _, v := range items {
				var parameters map[string]interface{}
				err = utils.JsonDecode(v.Parameters, &parameters)
				if err != nil {
					continue
				}
				tool := types.Tool{
					Type: "function",
					Function: types.Function{
						Name:        v.Name,
						Description: v.Description,
						Parameters:  parameters,
					},
				}
				// parameters["required"] 判断是否有 required 参数，如果没有就设置默认的 required 参数
				if v, ok := parameters["required"]; v == nil || !ok {
					parameters["required"] = []string{}
				}
				tools = append(tools, tool)
			}
			if len(tools) > 0 {
				req.Tools = tools
				req.ToolChoice = "auto"
			}
		}
	}
	// 加载聊天上下文
	chatCtx := make([]interface{}, 0)
	messages := make([]interface{}, 0)
	// 判断是否开启了聊天上下文
	if h.App.SysConfig.EnableContext {
		// 判断上下文中是否有会话的历史消息
		if h.ChatContexts.Has(session.ChatId) {
			// 获取会话历史消息
			messages = h.ChatContexts.Get(session.ChatId)
		} else {
			// 上下文中没有会话的历史消息，从应用的角色预料中获取
			_ = utils.JsonDecode(role.Context, &messages)
			// 判断会话上下文深度
			if h.App.SysConfig.ContextDeep > 0 {
				var historyMessages []model.ChatMessage
				// 查询指定深度的会话历史消息
				res := h.DB.Where("chat_id = ? and use_context = 1", session.ChatId).
					Limit(h.App.SysConfig.ContextDeep).Order("id desc").Find(&historyMessages)
				if res.Error == nil {
					for i := len(historyMessages) - 1; i >= 0; i-- {
						msg := historyMessages[i]
						ms := types.Message{Role: "user", Content: msg.Content}
						// 判断消息类型如果是大模型回复的消息就设置角色为 assistant
						if msg.Type == types.ReplyMsg {
							ms.Role = "assistant"
						}
						chatCtx = append(chatCtx, ms)
					}
				}
			}
		}
		// 最大响应长度
		tokens := req.MaxTokens
		// 计算调用工具消耗的token
		tks, _ := utils.CalcTokens(utils.JsonEncode(req.Tools), req.Model)
		tokens += tks + promptTokens

		for i := len(messages) - 1; i >= 0; i-- {
			v := messages[i]
			tks, _ = utils.CalcTokens(utils.JsonEncode(v), req.Model)
			// 上下文 token 超出了模型的最大上下文长度
			if tokens+tks >= session.Model.MaxContext {
				break
			}
			// 上下文的深度超出了模型的最大上下文深度
			if len(chatCtx) >= h.App.SysConfig.ContextDeep {
				break
			}
			tokens += tks
			chatCtx = append(chatCtx, v)
		}
		log.Debugf("聊天上下文：%+v", chatCtx)
	}

	reqMgs := make([]interface{}, 0)
	for i := len(chatCtx) - 1; i >= 0; i-- {
		reqMgs = append(reqMgs, chatCtx[i])
	}

	fullPrompt := prompt
	text := prompt

	// 从提示词中提取特定文件的URL
	files := utils.ExtractFileURLs(prompt)
	log.Debugf("从提示词中提取特定文件的URL：%v", files)
	// 如果不是逆向模型，则提取文件内容
	if len(files) > 0 && !(session.Model.Value == "gpt-4-all") ||
		strings.HasPrefix(session.Model.Value, "gpt-4-gizmo") ||
		strings.HasSuffix(session.Model.Value, "claude-3") {

		contents := make([]string, 0)
		var file model.File
		for _, v := range files {
			// 根据 URL 查询文件信息
			h.DB.Where("url = ?", v).First(&file)
			// 读取文件内容
			content, err := utils.ReadFileContent(v, h.App.Config.TikaHost)
			if err != nil {
				log.Errorf("读取文件出现错误：%v", err)
			} else {
				contents = append(contents, fmt.Sprintf("%s 文件内容：%s", file.Name, content))
			}
			// 把文件 URL 从提示词文本中去除掉
			text = strings.Replace(text, v, "", 1)
		}
		// 重新生成新的提示词
		if len(contents) > 0 {
			fullPrompt = fmt.Sprintf("请根据提供的文件内容信息回答问题(其中Excel 已转成 HTML)：\n\n %s\n\n 问题：%s", strings.Join(contents, "\n"), text)
		}
		// 重新计算消耗的token
		tokens, _ := utils.CalcTokens(fullPrompt, req.Model)
		if tokens > session.Model.MaxContext {
			return fmt.Errorf("文件的长度超出模型允许的最大上下文长度，请减少文件内容数量或文件大小。")
		}
	}

	log.Debugf("最终Prompt：%s", fullPrompt)
	// 提取提示词中的 图片 地址
	imgURLs := utils.ExtractImgURLs(prompt)
	log.Debugf("提示词中的图片地址：%+v", imgURLs)
	var content interface{}
	if len(imgURLs) > 0 {
		data := make([]interface{}, 0)
		// 变量图片 URL
		for _, v := range imgURLs {
			// 在 text 中查找子字符串 v，并将第一个匹配项替换为空字符串，1替换次数，表示仅替换第一个匹配项
			text = strings.Replace(text, v, "", 1)
			data = append(data, gin.H{
				"type": "image_url",
				"image_url": gin.H{
					"url": v,
				},
			})
		}
		data = append(data, gin.H{
			"type": "text",
			"text": strings.TrimSpace(text),
		})
		content = data
	} else {
		content = fullPrompt
	}

	req.Messages = append(reqMgs, map[string]interface{}{
		"role":    "user",
		"content": content,
	})
	log.Debugf("请求消息的上下文：%+v", req.Messages)
	return h.sendOpenAiMessage(ctx, req, userVo, session, role, prompt, ws)
}

// subUserPower 扣减用户算力
func (h *ChatHttpHandler) subUserPower(userVo vo.User, session *types.ChatSession, promptTokens int, replyTokens int) {
	power := 1
	// 设置模型消耗的算力
	if session.Model.Power > 0 {
		power = session.Model.Power
	}
	err := h.userService.DecreasePower(int(userVo.Id), power, model.PowerLog{
		Type:   types.PowerConsume,
		Model:  session.Model.Value,
		Remark: fmt.Sprintf("模型名称：%s, 提问长度：%d，回复长度：%d", session.Model.Name, promptTokens, replyTokens),
	})
	if err != nil {
		log.Errorf("扣减用户算力时出现错误：%v", err)
	}
}

func getTotalTokens(req types.ApiRequest) int {
	encode := utils.JsonEncode(req.Messages)
	// 消息切片map，每个消息是一个map
	var items []map[string]interface{}
	err := utils.JsonDecode(encode, &items)
	if err != nil {
		return 0
	}
	tokens := 0
	for _, item := range items {
		// 获取消息内容
		content, ok := item["content"]
		if ok && !utils.IsEmptyValue(content) {
			// 计算消息消耗的token数量
			t, err := utils.CalcTokens(utils.InterfaceToString(content), req.Model)
			if err == nil {
				tokens += t
			}
		}
	}
	return tokens
}

// saveChatHistory 保存会话历史消息
// 参数：promptCreatedAt 提问开始时间
// 参数：replyCreatedAt 模型响应时间
// 参数：message 大模型响应的消息
func (h *ChatHttpHandler) saveChatHistory(req types.ApiRequest, usage rsp.UsageResp, message types.Message, session *types.ChatSession,
	role model.ChatRole, userVo vo.User, promptCreatedAt time.Time, replyCreatedAt time.Time) {
	// 更新上下文信息
	if h.App.SysConfig.EnableContext {
		chatCtx := req.Messages
		// 把大模型响应的消息放入到 chatCtx 中
		chatCtx = append(chatCtx, message)
		// 在上下文中保存会话的消息
		h.ChatContexts.Put(session.ChatId, chatCtx)
	}

	// promptTokens 用户提示词消耗token
	// replyTokens 大模型响应消耗token
	// totalTokens 总消耗的token
	var promptTokens, replyTokens, totalTokens int

	// 计算用户提示词消耗token
	if usage.PromptTokens > 0 {
		promptTokens = usage.PromptTokens
	} else {
		promptTokens, _ = utils.CalcTokens(usage.Prompt, req.Model)
	}

	// 用户提问消息
	historyUserMsg := model.ChatMessage{
		UserId:      userVo.Id,
		ChatId:      session.ChatId,
		RoleId:      role.Id,
		Type:        types.PromptMsg,
		Icon:        userVo.Avatar,
		Content:     template.HTMLEscapeString(usage.Prompt), // 对字符串进行 HTML 转义
		Tokens:      promptTokens,
		TotalTokens: promptTokens,
		UseContext:  true,
		Model:       req.Model,
	}
	historyUserMsg.CreatedAt = promptCreatedAt
	historyUserMsg.UpdatedAt = promptCreatedAt
	// 保存用户提问消息
	err := h.DB.Save(&historyUserMsg).Error
	if err != nil {
		log.Errorf("保存用户提示词消息失败：%v", err)
	}

	// 计算本次对话消耗的总 token 数量
	if usage.CompletionTokens > 0 {
		replyTokens = usage.CompletionTokens
		totalTokens = usage.TotalTokens
	} else {
		// 计算模型输出内容消耗的token
		replyTokens, _ = utils.CalcTokens(usage.Content, req.Model)
		// replyTokens（模型响应token） + getTotalTokens(req)（用户请求输入的提示词token）
		totalTokens = replyTokens + getTotalTokens(req)
	}

	// 大模型响应消息
	historyReplyMsg := model.ChatMessage{
		UserId:      userVo.Id,
		ChatId:      session.ChatId,
		RoleId:      role.Id,
		Type:        types.ReplyMsg,
		Icon:        role.Icon,
		Content:     usage.Content,
		Tokens:      replyTokens,
		TotalTokens: totalTokens,
		UseContext:  true,
		Model:       req.Model,
	}
	historyReplyMsg.CreatedAt = replyCreatedAt
	historyReplyMsg.UpdatedAt = replyCreatedAt
	// 保存模型响应消息
	err = h.DB.Save(&historyReplyMsg).Error
	if err != nil {
		log.Errorf("保存大模型响应消息失败：%v", err)
	}

	// 更新用户算力
	if session.Model.Power > 0 {
		h.subUserPower(userVo, session, promptTokens, replyTokens)
	}
	// 保存当前会话
	var chatItem model.ChatItem
	// 查询会话信息
	err = h.DB.Where("chat_id = ?", session.ChatId).First(&chatItem).Error
	// err != nil 表示没有找到会话，用户第一次进行提问，就新建会话
	if err != nil {
		chatItem.ChatId = session.ChatId
		chatItem.UserId = userVo.Id
		chatItem.RoleId = role.Id
		chatItem.ModelId = session.Model.Id
		// 判断用户输入的提示词是否大于 30，如果大于就截取，我们使用用户的提示词作为会话的标题
		// utf8.RuneCountInString 该函数通过解析 UTF-8 编码的字符串，返回其中包含的 Unicode 字符（码位）的数量。例如，中文字符在 UTF-8 中通常占用 3 字节，但会被计为 1 个字符
		if utf8.RuneCountInString(usage.Prompt) > 30 {
			// rune 是 int32 的别名（type rune = int32），用于表示一个 Unicode 码点（Code Point），支持所有 Unicode 字符，包括多字节字符（如中文、Emoji 等）
			// 字符串本质是 UTF-8 编码的字节序列，而 []rune 是 Unicode 码点的切片
			// []rune(s) 会将字符串按 Unicode 字符逐个解码为码点，生成对应的 int32 切片
			chatItem.Title = string([]rune(usage.Prompt)[:30]) + "..."
		} else {
			chatItem.Title = usage.Prompt
		}
		chatItem.Model = req.Model
		// 保存会话
		err = h.DB.Create(&chatItem).Error
		if err != nil {
			log.Errorf("保存用户会话失败：%v", err)
		}
	}
}

// extractImgUrl 将 AI 回复消息中生成的图片链接下载到本地
func (h *ChatHttpHandler) extractImgUrl(text string) string {
	// ([^\]]*)：非贪婪匹配替代文本（alt），避免匹配到右括号 ]
	// 匹配URL部分，直到遇到右括号 )，确保不会包含多余字符
	patten := `!\[([^\]]*)]\(([^)]+)\)`
	re := regexp.MustCompile(patten)
	// 返回所有匹配项及其子分组，matches是一个二维数组，每行包含完整匹配、alt、URL
	matches := re.FindAllStringSubmatch(text, -1)
	// 下载图片并替换链接地址
	for _, match := range matches {
		imageURL := match[2]
		log.Debugf("AI 回复消息中生成的图片链接：%s", imageURL)
		// 对于相同地址的图片，已经被替换了，就不在重复下载了
		// 若包含，则说明该图片链接 imageURL 尚未被替换，需要继续处理；
		// 若不包含，则说明已被替换过，跳过当前循环。
		if !strings.Contains(text, imageURL) {
			continue
		}
		// 下载图片
		newImgURL, err := h.uploadManager.GetUploadHandler().PutUrlFile(imageURL, true)
		if err != nil {
			log.Errorf("下载图片出现错误：%v", err)
			continue
		}
		// strings.ReplaceAll 把所有匹配的imageURL，替换成 newImgURL，然后生成新字符串
		text = strings.ReplaceAll(text, imageURL, newImgURL)
	}
	return text
}

// sendOpenAiMessage OpenAI 消息发送实现
func (h *ChatHttpHandler) sendOpenAiMessage(ctx context.Context, req types.ApiRequest, userVo vo.User, session *types.ChatSession,
	role model.ChatRole, prompt string, ws *types.WsClient) error {
	// 记录提问时间
	promptCreatedAt := time.Now()
	// 开始发送请求时间
	start := time.Now()
	var apiKey = model.ApiKey{}
	// 发送请求
	response, err := h.doRequest(ctx, req, session, &apiKey)
	log.Infof("HTTP请求完成，耗时：%d", time.Now().Sub(start))
	// 判断请求方法是否出错
	if err != nil {
		if strings.Contains(err.Error(), "context canceled") {
			return fmt.Errorf("用户取消了请求：%s", prompt)
		} else if strings.Contains(err.Error(), "no available key") {
			return errors.New("抱歉😔😔😔，系统已经没有可用的 API KEY，请联系管理员！")
		}
		return err
	} else {
		defer response.Body.Close()
	}

	// 判断请求响应状态码是否正确
	if response.StatusCode != 200 {
		body, _ := io.ReadAll(response.Body)
		return fmt.Errorf("请求 %s 大模型 API 失败：%d，%v", req.Model, response.StatusCode, string(body))
	}

	contentType := response.Header.Get("Content-Type")
	// 判断响应是不是流式输出
	if strings.Contains(contentType, "text/event-stream") {
		// 记录回复时间
		replyCreatedAt := time.Now()
		// 循环读取 Chunk 消息
		var message = types.Message{Role: "assistant"}
		// 存储我们读取出来的内容
		var contents = make([]string, 0)
		// 接收函数信息
		var function model.Function
		// 函数是否调用完成
		var toolCall = false
		var arguments = make([]string, 0)
		// 高效处理 HTTP 响应体的方式，适用于流式数据的逐行读取
		scanner := bufio.NewScanner(response.Body)
		// scanner.Scan() 尝试从输入流中读取下一个数据块（默认按行分割，即遇到 \n 时结束）
		// 				  true：成功读取到数据，可通过 Text() 获取内容
		//			      false：输入流结束或发生错误（需通过 scanner.Err() 检查错误）
		for scanner.Scan() {
			// 返回 Scan() 方法最新读取的字符串内容，即当前行或当前分割块
			line := scanner.Text()
			log.Infof("大模型返回的消息：%s", line)
			// 流式数据响应，就是这种格式
			// data: {"id":"8313807536837492492","created":1706092316,"model":"glm-4-plus","choices":[{"index":0,"delta":{"role":"assistant","content":"土"}}]}
			// data: {"id":"8313807536837492492","created":1706092316,"model":"glm-4-plus","choices":[{"index":0,"delta":{"role":"assistant","content":"主要由"}}]}
			// data: {"id":"8313807536837492492","created":1706092316,"model":"glm-4-plus","choices":[{"index":0,"finish_reason":"length","delta":{"role":"assistant","content":""}}],"usage":{"prompt_tokens":60,"completion_tokens":100,"total_tokens":160}}
			// data: [DONE]
			// 判断响应数据是否正确
			if !strings.Contains(line, "data:") || len(line) < 30 {
				continue
			}

			var responseBody = types.ApiResponse{}
			// line[6:] 截取掉 data:
			err = json.Unmarshal([]byte(line[6:]), &responseBody)
			// 数据解析出错
			if err != nil {
				return errors.New(fmt.Sprintf("响应的数据格式不正确，解析出错：%s", line))
			}
			// Fixed: 兼容 Azure API 第一个输出空行
			if len(responseBody.Choices) == 0 {
				continue
			}
			// 判断 Content 和 ToolCalls 都为空 没有返回数据
			if responseBody.Choices[0].Delta.Content == nil && responseBody.Choices[0].Delta.ToolCalls == nil {
				continue
			}

			// 判断 FinishReason 是否等于 stop，如果等于就表示大模型停止输出，判断 contents 是否等于0，如果等于就表示大模型第一次响应就停止了
			if responseBody.Choices[0].FinishReason == "stop" && len(contents) == 0 {
				utils.SendChunkMsg(ws, "抱歉😔😔😔，AI助手由于未知原因已经停止输出内容。")
				break
			}

			// 声明工具调用对象
			var tool types.ToolCall
			// 检查当前流式数据块中是否存在工具调用（ToolCalls 切片非空），并提取第一个工具调用对象
			if len(responseBody.Choices[0].Delta.ToolCalls) > 0 {
				tool = responseBody.Choices[0].Delta.ToolCalls[0]
				// 当工具调用未完成（toolCall 为真）且工具名称未设置时，将增量参数追加到 arguments 切片中，实现参数拼接。
				// OpenAI 流式响应中，工具调用的 Name 可能仅在首个数据块出现，而 Arguments 会分多次传输
				if toolCall && tool.Function.Name == "" {
					arguments = append(arguments, tool.Function.Arguments)
					continue
				}
				arguments = append(arguments, tool.Function.Arguments)
			}

			// 兼容 Function Call
			// 从流式响应当前数据块中提取函数调用信息。
			fun := responseBody.Choices[0].Delta.FunctionCall
			// 若当前数据块包含函数名（Name 非空），则初始化新工具对象并记录名称。
			if fun.Name != "" {
				// *new(types.ToolCall) 等价于 types.ToolCall{}，初始化空结构体。
				// 函数名通常仅在首个数据块中出现一次。
				tool = *new(types.ToolCall)
				tool.Function.Name = fun.Name
			} else if toolCall { // 若当前处于工具调用处理中（toolCall 为 true），且数据块包含参数片段（Arguments），则持续收集。
				arguments = append(arguments, fun.Arguments)
				continue
			}

			// 判断工具调用对象是否为空
			if !utils.IsEmptyValue(tool) {
				// 查询函数插件
				res := h.DB.Where("name = ?", tool.Function.Name).First(&function)
				if res.Error == nil {
					toolCall = true
					callMsg := fmt.Sprintf("正在调用工具 `%s` 作答 ...\n\n", function.Label)
					// 响应消息
					utils.SendChunkMsg(ws, callMsg)
					contents = append(contents, callMsg)
				}
			}

			// 函数或者工具调用完成
			if responseBody.Choices[0].FinishReason == "tool_calls" || responseBody.Choices[0].FinishReason == "function_call" {
				break
			}

			if responseBody.Choices[0].FinishReason != "" {
				// 输出完成或者输出中断了
				break
			} else {
				// 获取大模型回复的内容
				content := responseBody.Choices[0].Delta.Content
				// 存储内容起来
				contents = append(contents, utils.InterfaceToString(content))
				// 给前端响应内容
				utils.SendChunkMsg(ws, content)
			}
		}

		// 输入流结束
		if err := scanner.Err(); err != nil {
			if strings.Contains(err.Error(), "context canceled") {
				log.Warnf("用户取消了请求：%s", prompt)
			} else {
				log.Errorf("读取消息出错：%v", err)
			}
		}

		// 调用函数
		if toolCall {
			params := make(map[string]interface{})
			_ = utils.JsonDecode(strings.Join(arguments, ""), &params)
			log.Debugf("函数名称：%s，函数参数：%s", function.Name, params)
			params["user_id"] = userVo.Id
			var apiRes types.BizVo
			r, err := req2.C().R().SetHeader("Body-Type", "application/json").
				SetHeader("Authorization", function.Token).SetBody(params).SetSuccessResult(&apiRes).Post(function.Action)
			errMsg := ""
			if err != nil {
				errMsg = err.Error()
			} else {
				all, _ := io.ReadAll(r.Body)
				err = json.Unmarshal(all, &apiRes)
				if err != nil {
					errMsg = err.Error()
				} else if apiRes.Code != types.Success {
					errMsg = apiRes.Message
				}
			}
			if errMsg != "" {
				errMsg = "调用函数工具出错：" + errMsg
				contents = append(contents, errMsg)
			} else {
				errMsg = utils.InterfaceToString(apiRes.Data)
				contents = append(contents, errMsg)
			}
			utils.SendChunkMsg(ws, errMsg)
		}

		// 消息发送成功
		if len(contents) > 0 {
			usage := rsp.UsageResp{
				Prompt:           prompt,
				Content:          strings.Join(contents, ""),
				PromptTokens:     0,
				CompletionTokens: 0,
				TotalTokens:      0,
			}
			message.Content = usage.Content
			// 保存会话聊天记录
			h.saveChatHistory(req, usage, message, session, role, userVo, promptCreatedAt, replyCreatedAt)
		}
	} else {
		// 非流式输出
		var openAiResp rsp.OpenAIResp
		body, err := io.ReadAll(response.Body)
		if err != nil {
			return fmt.Errorf("读取响应内容失败：%v", err)
		}
		err = json.Unmarshal(body, &openAiResp)
		if err != nil {
			return fmt.Errorf("解析响应失败，格式不正确：%v，body：%v", err, body)
		}
		content := openAiResp.Choices[0].Message.Content
		if strings.HasPrefix(req.Model, "o1-") {
			content = fmt.Sprintf("AI思考结束，耗时：%d 秒。\n%s", time.Now().Unix()-session.Start, openAiResp.Choices[0].Message.Content)
		}
		// 往前端响应消息
		utils.SendChunkMsg(ws, content)
		openAiResp.Usage.Prompt = prompt
		openAiResp.Usage.Content = content
		// 保存会话聊天记录
		h.saveChatHistory(req, openAiResp.Usage, openAiResp.Choices[0].Message, session, role, userVo, promptCreatedAt, time.Now())
	}
	return nil
}

// 发送对话请求到 大模型 服务器（不仅限于 OpenAI 大模型）
func (h *ChatHttpHandler) doRequest(ctx context.Context, req types.ApiRequest,
	session *types.ChatSession, apiKey *model.ApiKey) (*http.Response, error) {
	log.Infof("大模型请求参数：%+v", req)
	// 会话模型绑定了 API KEY，直接使用它
	if session.Model.KeyId > 0 {
		h.DB.Where("id", session.Model.KeyId).Find(apiKey)
	}

	// 判断秘钥是否存在，如果不存在查找最后一个未使用的秘钥
	if apiKey.Id == 0 {
		h.DB.Where("type", "chat").Where("enabled", true).Order("last_used_at asc").First(apiKey)
	}

	if apiKey.Id == 0 {
		return nil, errors.New("no available key 没有查询到可用的秘钥，请添加秘钥。")
	}

	log.Debugf("【openai_handler.doRequest】聊天对话请求消息体：%+v", req)
	// 拼接请求 地址
	apiURL := fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL)
	var client *http.Client
	// 请求参数转换
	requestBody, err := json.Marshal(req)
	if err != nil {
		return nil, err
	}
	// 创建请求对象
	request, err := http.NewRequest(http.MethodPost, apiURL, bytes.NewBuffer(requestBody))
	if err != nil {
		return nil, err
	}

	request = request.WithContext(ctx)
	request.Header.Set("Content-Type", "application/json")
	// 判断是否使用代理
	if len(apiKey.ProxyURL) > 5 {
		proxy, _ := url.Parse(apiKey.ProxyURL)
		client = &http.Client{
			Transport: &http.Transport{
				Proxy: http.ProxyURL(proxy),
			},
		}
	} else {
		client = http.DefaultClient
	}

	log.Infof("发送请求：%s，API KEY：%s，Proxy：%s，Model：%s", apiURL, apiKey.ApiURL, apiKey.ProxyURL, req.Model)
	// 设置秘钥
	request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey.Value))
	// 更新 API KEY 最后使用时间
	h.DB.Model(&model.ApiKey{}).Where("id", apiKey.Id).UpdateColumn("last_used_at", time.Now().Unix())
	// 发送请求
	return client.Do(request)
}
